本文主要介绍Matrix的Resource部分,涉及Activity泄漏、Bitmap对象冗余的检测逻辑。
再回顾下Matrix概览中提到的ResourceCanary模块的特点:基于 WeakReference 的特性和 Square Haha 库开发的 Activity 泄漏和 Bitmap 重复创建检测工具。
主要特性如下:
- 分离了检测和分析部分,便于在不打断自动化测试的前提下持续输出分析后的检测结果
- 对检测部分生成的 Hprof 文件进行了裁剪,移除了大部分无用数据,降低了传输 Hprof 文件的开销
- 增加了重复 Bitmap 对象检测,方便通过减少冗余 Bitmap 数量,降低内存消耗
如何判断Activity发生泄漏
判断Activity是否泄漏需要确定两个问题:
- 如何在一个恰当的时机得知一个Activity已经结束了生命周期
- 如何判断一个Activity无法被GC机制回收
这部分内容,Matrix借鉴了LeakCanary的做法,对LeakCanary比较熟悉的同学可以跳过。后续将针对这两个问题进行实现分析。
Demo中,TestLeakActivity
被一个静态变量testLeaks
持有:
1 | public class TestLeakActivity extends Activity { |
测试结果是,退出页面时TestLeakActivity
无法正常销毁,页面发生泄漏。
通过操作demo,可以看到如下IDE的输出log:
1 | V/Matrix.ActivityRefWatcher: triggering gc... |
上面log意思是TestLeakActivity
应该被回收但是仍然存在,即发生了泄漏。
获取已销毁Activity的信息
针对第一个问题“如何在一个恰当的时机得知一个Activity已经结束了生命周期”,我们看看Matrix怎么做的。
Matrix wiki中也提到的了解决方法:
- 让所有Activity继承一个
BaseActivity
,然后在BaseActivity.onDestroy()
方法中进行记录。 - 通过某种机制得知
Activity.onDestroy()
方法被调用,然后进行记录- 4.0以前可以通过反射替换
ActivityThread.mInstrumentation
对象为自己的代理,然后在代理中的callActivityOnDestroy()
方法中记录。 - 4.0以后可以通过
Application.registerActivityLifecycleCallbacks()
方法注册一个回调对象,在回调对象的onActivityDestroyed()
方法中记录。
- 4.0以前可以通过反射替换
显然第一种方法对业务侧入侵过重,不合适。第二种方法,考虑到4.0以前机器分布已经比较少了,Matrix直接使用Application.registerActivityLifecycleCallbacks()
方式。
ResouceCanary中处理Activity泄漏问题的接口类是ResourcePlugin,看下代码:
1 | com.tencent.matrix.resource.ResourcePlugin |
上面代码在组件启动时,初始化了ActivityRefWatcher
,看下mWatcher.start()
:
1 | com.tencent.matrix.resource.watcher.ActivityRefWatcher |
上面代码主要做了注册Activity生命周期监听器和Activity泄漏检测任务调度这两件事。其中Activity泄漏检测任务调度的逻辑在后面《如何判断Activity无法被GC回收》会讲到。看下mRemovedActivityMonitor
的逻辑:
1 | com.tencent.matrix.resource.watcher.ActivityRefWatcher |
当一个Activity销毁时,即Activity.onDestroy()
执行时,Application.ActivityLifecycleCallbacks.onActivityDestroyed()
就会被调用,看下pushDestroyedActivityInfo()
:
1 | com.tencent.matrix.resource.watcher.ActivityRefWatcher |
上面代码主要做了两件事:
- 通过
isPublished()
优化上报逻辑:泄漏的Activity,记录其类名,放到FilePublisher.mPublishedMap
,后续再检测到该Activity且在过期时间内,则走去重逻辑,避免重复提示该Activity已泄漏 - 根据UUID和Activity信息组装key,将泄漏的Activity信息存放到
mDestroyedActivityInfos
,其中DestroyedActivityInfo
内部使用一个WeakReference对象持有该Activity
总结:
针对“如何在一个恰当的时机得知一个Activity已经结束了生命周期”,Matrix和LeakCanary的逻辑一样,通过Application.registerActivityLifecycleCallbacks()
方法注册一个回调对象,在回调对象的onActivityDestroyed()
方法中记录泄漏页面信息。
如何判断Activity无法被GC回收
Activity被销毁后,如何判断其是否发生了泄漏呢?针对前面提到的第二个问题“如何判断一个Activity无法被GC机制回收”,Matrix的做法大致是这样的:
首先根据前面《获取已销毁Activity的信息》部分获取destroy的Activity,该Activity由WeakReference持有,然后主动触发一次“有效的GC”,如果该Activity能够被回收,则持有它的WeakReference会被置空;反之,如果持有它的WeakReference不为空,即GC无法回收这个已经销毁的Activity,判定该Activity发生了泄漏。
上面说的“有效的GC”,是因为JVM没有提供强制触发GC的API,像System.gc()
或者Runtime.getRuntime().gc()
都是建议系统进行GC,系统并不一定真正的触发GC。
针对这个问题,Matrix使用了“哨兵机制”,即增加了一个“哨兵对象”,该对象由WeakReference持有,执行Runtime.getRuntime().gc()
后,如果该哨兵WeakReference被置空,则说明刚才的gc()调用,系统确实触发了一次GC操作。
看下这部分逻辑的代码,实现细节在ActivityRefWatcher.scheduleDetectProcedure()
:
1 | com.tencent.matrix.resource.watcher.ActivityRefWatcher |
看下mDetectExecutor.executeInBackground()
:
1 | com.tencent.matrix.resource.watcher.RetryableTaskExecutor |
上面代码就是在异步线程执行mScanDestroyedActivitiesTask
,当该task返回值为RetryableTask.Status.RETRY
时,则该task进行延时重试。看下该task内容:
1 | com.tencent.matrix.resource.watcher.ActivityRefWatcher |
上面这部分代码主要做了8件事,注释中都一一标明,本质上是利用“哨兵”机制和去重策略,准确的找到无法被GC回收的已销毁Activity,然后再根据外部配置的DumpMode输出泄漏Activity的信息。
测试内存泄漏时,需要在MatrixApplication
中ResourceCanary配置初始化过程中,将ResourceConfig.Builder.setDetectDebuger()
传true才能看到具体的泄漏信息。比如我们把DumpMode
改成MANUAL_DUMP
,测试TestLeakActivity
时,可以在通知栏看到如下信息,即TestLeakActivity
发生了泄漏。
Hprof
在分析泄漏对象信息及引用链时,需要对Hprof有一些背景知识,下面介绍下Hprof相关内容。
Hprof文件也叫堆转储文件,包含了dump时刻内存中的所有对象的信息,包括类的描述、实例的数据和引用关系、线程的栈信息等。
如何获取Hprof文件
代码中可以使用Debug.dumpHprofData(file)
方法获取Hprof文件。
平时开发定位问题,可以直接用AndroidStudio中的Profiler获取hprof文件。官方文档使用 Memory Profiler 查看 Java 堆和内存分配比较详细的介绍了hprof的使用方法,具体可看官网。如果AS底部控制台没有Profiler选项,也可以通过AndroidStudio顶部Run菜单里面的profiler选项启动profiler。
Profiler目前已经是非常强大且简单易用的性能分析工具,可以分析CPU使用率、内存占用、网络使用、方法耗时等各种指标。笔者平时跟踪代码,获取方法调用栈、分析方法耗时等,也经常使用该工具,比如之前分析Matrix-Trace源码分析文章时,通过Profiler的方法调用栈就比较容易的看到哪些方法是通过插桩实现的,也方便快速熟悉陌生的项目,建议大家多学习使用该工具。
借用官网中提供的图片,下图中的“2”可dump出hprof文件。开发过程中,rocord一段App操作后,可以把操作后的堆栈信息用这种方式dump出来,后续便可使用AS导入hprof文件,方便分析。
将本地Hprof文件导入Profiler中:
上图中各个列的含义:
- Allocations:堆中的分配数。
- Native Size:此对象类型使用的原生内存总量(以字节为单位)。只有在使用 Android 7.0 及更高版本时,才会看到此列。您会在此处看到采用 Java 分配的某些对象的内存,因为 Android 对某些框架类(如
Bitmap
)使用原生内存。 - Shallow Size:此对象类型使用的 Java 内存总量(以字节为单位)。
- Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)。
如何获取引用链
前面提到如何获取泄漏Activity的信息,定位问题时,还需要计算泄漏Activity对象到GCRoots的强引用链。GCRoots对象特点是,他们虽然不被其他生命周期更长的对象持有,但JVM特性导致这类对象不会被GC回收。因此,从这类对象出发,经过一系列强引用的对象也都无法被回收。这部分内容可以看看这篇文章。
GCRoots包括下面5类对象:
- 静态成员,因为Java中的类,即被JVM system class loader加载的类无法卸载,也就无法被回收,典型的就是类的静态成员以及被静态成员持有的对象都是无法被GC的
- 局部变量或方法参数持有的对象
- JNI Reference,包括JNILocalReference、JNIGlobalReference持有的对象
- 活动的Thread实例
- synchronized关键字用到的对象
如果某个Activity被泄漏,则必然存在从它到某个GC Root的强引用链。只要将这条强引用链找出来,开发者就能根据引用链上的对象找到合适的修改点快速解决问题。
Hprof文件格式可以参考这份文档Binary Dump Format一节,文档中GCRoots的Tag格式如下:
按照文档描述的格式将Hprof中的实例信息解析成对象引用关系的图结构后,套用经典的图搜索算法即可找到泄漏的Activity到GC Root的强引用链。这部分代码实现,老版的LeakCanary和Matrix都借鉴了HAHA库的逻辑。目前这个Repo已经被标记为DEPRECATED,最新版的LeakCanary 2自己实现了名叫Shark的heap分析库,这个库使用Kotlin实现。
Hprof文件分析
Matrix ResourceCanary将检测Activity泄漏和分析泄漏对象的信息整个过程,分成检测和分析两个阶段。前者因为检测Activity泄漏包含很多系统概念,只能在端上做,但是分析阶段是一个处理hprof文件的过程,可以拆出来。拆出来的好处是:
- 更新分析逻辑不再需要重新发客户端版本
- Hprof文件留在了服务端,为人工分析提供了机会
- 如果跳过触发Dump Hprof,甚至可以把监测步骤在现网环境启用,以发现测试阶段难以触发的Activity泄漏
前面检测阶段,会生成一个压缩包,内部包含一个hrof文件和一个result.info
文件,result.info
内容示例如下:
1 | # Resource Canary Result Infomation. THIS FILE IS IMPORTANT FOR THE ANALYZER !! |
hprofEntry就是创建的hprof文件名,leakedActivityKey是leak activity的信息。
hprof文件分析的代码在matrix-resouce-canary-analyzer
这个module里面,入口方法是CLIMain.main()
,下面来具体看下。
1 | com.tencent.matrix.resource.analyzer.CLIMain |
上面代码先对命令行参数进行解析,入参配置可以参考下面:
1 | args : { |
- -i:检测阶段生成的hprof压缩包文件路径,该参数必选
- -o:分析结果输出文件路径,该参数必选
- -co:是否将分析结果输出文件做成压缩文件,该参数可选
- -mb:触发bitmap泄漏警告的最小尺寸,默认阈值时5000,该参数可选
这几个参数的含义可以参考CLIMain.AnalyzerOptions
。
参数解析完后,执行doAnalyze()
进行具体分析逻辑。
1 | com.tencent.matrix.resource.analyzer.CLIMain |
上面代码主要是解析检测阶段生成的result.info文件的数据,后续通过analyzeAndStoreResult()
进行分析。
1 | com.tencent.matrix.resource.analyzer.CLIMain |
上面先在HeapSnapshot(File hprofFile)
内部使用HAHA库解析Hprof文件,再通过ActivityLeakAnalyzer().analyze()
分析泄漏Activity的最短强引用链。
分析结果中,ActivityLeakResult
中referenceChain
内容如下:
1 | * GC ROOT android.os.HandlerThread contextClassLoader |
最后一行显示sample.tencent.matrix.resource.TestLeakActivity
发生泄漏,从下到上是TestLeakActivity
到GCRoots的强引用链。从第四行可以看到静态变量testLeaks
,这个检测结果和前面讲到静态变量testLeaks
持有TestLeakActivity
导致其无法释放的结论是一致的。
Hprof文件格式说明
在介绍haha库如何解析Hprof之前,先简单介绍下Hprof文件的结构。
Hprof文件主要由一系列的record组成,每一个record的内容结构如下:
每一个record包含四部分内容:
- TAG:该record内容的类型,比如string数据是0x01,LOAD ClASS是0x02,栈帧Stack Frame是0x04
- TIME:时间戳,作用不大,占4个字节,代码中也忽略不用
- LENGTH:表示body的长度
- BODY:实体内容
其中u1、u2、u4、u8代表几个byte,比如u4就代表占用4个字节。
每一种record类型都对应着一个tag,目前支持的tag部分内容如下,具体参考HPROF Agent Binary Dump Format这一节:
比如string数据是0x01,LOAD ClASS是0x02,stack frame是0x04,stack trace是0x05等等。
HAHA库解析Hprof
先看下HAHA库的解析逻辑:
1 | com.tencent.matrix.resource.analyzer.model.HeapSnapshot |
上面代码重点在HprofParser.parse()
,当前Matrix版本使用haha-2.0.3版本:
1 | com.squareup.haha.perflib.HprofParser |
上面parse()
中的while循环,开始都会先读取TAG、TIME、LENGTH这三个部分的内容,然后再根据tag和length解析body,即对应类型的值:
1 | int tag = readUnsignedByte(); |
拿stack frame举例:
栈帧中包含方法名、签名、源文件名、类序列号、行号等信息。
通过下面代码可以看到如何获取body中的信息:
1 | com.squareup.haha.perflib.HprofParser |
上面每一行的内容和文档中对STACK FRAME的格式描述严格一致。所以,为什么说HAHA库是把Hprof这种文档描述结构转义成了引用关系结构,也是这个原因。
如何检测重复Bitmap对象
这部分内容,实现思路wiki中的描述如下:原理简单粗暴——把所有未被回收的Bitmap的数据buffer取出来,然后先对比所有长度为1的buffer,找出相同的,记录所属的Bitmap对象;再对比所有长度为2的、长度为3的buffer……直到把所有buffer都比对完,这样就记录下了所有冗余的Bitmap对象了,接着再套用LeakCanary获取引用链的逻辑把这些Bitmap对象到GC Root的最短强引用链找出来即可。